import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { PRIORITY_LABELS } from "@websensor/core/client"; import { EventRow } from "@/components/event-row"; import { FieldChangeInline } from "@/components/field-changes"; import { Chip, Empty, ExtLink, HealthPill, PageHeader, Panel, Stat, Table, Td, TierBadge } from "@/components/ui"; import { api, type SnapshotRow } from "@/lib/api"; import { CLASS_LABELS, dayHeader, fmtBytes, fmtDuration, fmtInt, fmtMs, relTime, shortHash, untilTime, utcDate, utcDateTime, utcTime } from "@/lib/format"; export const dynamic = "force-dynamic"; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params; const d = await api.sensor(id); return { title: d ? `${d.sensor.source_name} · ${d.sensor.name} — sensor` : "Sensor not found", robots: { index: false } }; } /** Sensor page with historical memory (spec §79): how this page looked over time. */ export default async function SensorPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const d = await api.sensor(id); if (!d) notFound(); const s = d.sensor; const snaps = await api.sensorSnapshots(s.id, 200); const history = snaps.items.length ? snaps.items : d.snapshots; const runs = s.total_runs ?? 0; const checks24 = s.checks_24h ?? 0; const nm24 = checks24 ? Math.round(((s.not_modified_24h ?? 0) / checks24) * 100) : null; const nmAll = runs ? Math.round(((s.total_not_modified ?? 0) / runs) * 100) : null; const events = d.events ?? []; const prio = s.priority ?? null; // Group snapshots by UTC day (newest first, API order). const days: { day: string; rows: SnapshotRow[] }[] = []; for (const sn of history) { const day = utcDate(sn.captured_at); const last = days[days.length - 1]; if (last && last.day === day) last.rows.push(sn); else days.push({ day, rows: [sn] }); } const changeCount = history.filter((x) => x.has_change).length; return ( <> {s.source_name} / {s.type} · {s.connector} {prio !== null && P{prio}} {s.status && } } title={s.name} description={{s.url}} actions={ <> URL history → {s.domain && Domain →} } />
0 ? "warn" : undefined} hint={s.consecutive_errors ? {s.consecutive_errors} consecutive : "0 consecutive"} /> {fmtInt(s.raw_changes)} / {fmtInt(s.meaningful_changes)}} hint={s.raw_changes ? `noise ${Math.round((1 - (s.meaningful_changes ?? 0) / Math.max(1, s.raw_changes)) * 100)}%` : undefined} />
{s.last_error && (
last error · {s.last_error}
)}
raw bodies may be pruned by retention · canonical form and hashes are kept}> {history.length === 0 ? ( No snapshot yet — the first successful check stores the initial state of this page. ) : (
{["Captured", "HTTP", "Size", "Canonical hash", "Change", "Event", "Raw", "Compare"].map((h) => ( ))} {days.map((g) => ( {g.rows.map((sn) => { const idx = history.indexOf(sn); const prev = history[idx + 1]; return ( ); })} ))}
{h}
{dayHeader(g.rows[0]!.captured_at)} · {g.rows.length} capture{g.rows.length === 1 ? "" : "s"}
{utcTime(sn.captured_at)} {relTime(sn.captured_at)} = 400 ? "text-warn" : "text-fg-muted"}>{sn.http_status ?? "—"} {fmtBytes(sn.content_length)} {shortHash(sn.canonical_hash)}{sn.mode ? {sn.mode} : null} {sn.has_change ? CHANGED : —} {sn.event_slug ? event → : —} {sn.has_raw === false ? ( view raw ) : ( view raw )} {prev ? compare with previous : first capture}
)}
{d.changes.length ? ( {d.changes.map((c) => ( ))}
{utcDate(c.detected_at)} {utcTime(c.detected_at, false)} {c.kind} {c.change_class ? {CLASS_LABELS[c.change_class] ?? c.change_class} : c.heuristic_type ? {c.heuristic_type.replace(/_/g, " ")} : —} = 0.32 ? "text-signal" : "text-fg-subtle"}>{c.signal.toFixed(2)}{c.noise_ratio !== undefined && c.noise_ratio > 0 ? noise {Math.round(c.noise_ratio * 100)}% : null} {c.meaningful ? YES : filtered} {c.field_changes?.length ? : c.magnitude !== undefined && c.magnitude !== null ? magnitude {c.magnitude} : —} {c.event_id && event} {c.event_id && c.old_snapshot_id && c.new_snapshot_id && · } {c.old_snapshot_id && c.new_snapshot_id && diff} {!c.event_id && !(c.old_snapshot_id && c.new_snapshot_id) && —}
) : ( No change detected yet. )}
{d.runs.length ? ( {d.runs.map((r) => ( ))}
{utcDateTime(r.started_at)} {r.outcome} {r.http_status ?? "—"} {fmtMs(r.duration_ms)} {fmtBytes(r.bytes)} {r.fetch_method ?? "—"} {r.error ?? ""}
) : ( No run recorded in the retained window. )}
); }